" File: pdm.vim
" Author: Jeffy Du <cmdxiaoha@163.com>
" Version: 0.1
" Last Modified: <2011-10-24 09:48:51 by Jeffy Du>
" Copyright: Copyright (C) 2011 Jeffy Du
"            Permission is hereby granted to use and distribute this code,
"            with or without modifications, provided that this copyright
"            notice is copied with it. Like anything else that's free,
"            pdm.vim is provided *as is* and comes with no warranty of any
"            kind, either expressed or implied. In no event will the copyright
"            holder be liable for any damamges resulting from the use of this
"            software.
"
" Overview
" --------
" The Project Database (PDM) plugin provides an easy operation to a c/c++
" project database in Vim. This plugin create/load/update a c/c++ project 
" database and some other operations in Vim.
"
" This plugin work with ctags/cscope/find/grep/sort/ system-callings. 
" If you run this plugin " in Win32 system, your must add these 
" system-callings to system path. Usually, you can find these 
" system-callings in Cygwin.
"
" The project database can use to other plugins, such as lookupfile,
" omnicppcompl...
"
" Installation
" ------------
" 1. Copy the pdm.vim file to one of the following directories:
"
"       $HOME/.vim/plugin    - Unix like systems
"       $VIM/vimfiles/plugin - MS-Windows
"
"    Refer to the following Vim help topics for more information about Vim
"    plugins:
"
"       :help add-plugin
"       :help add-global-plugin
"       :help runtimepath
"
" 2. Start Vim, run command "PDMCreate", complete create PDM step by step.
" 3. Run command "PDMLoad", complete load PDM step by step.
" 4. Now, you can use the filenametags, tags, cscope data.
"
" To uninstall this plugin, remove this file (pdm.vim) from the
" $HOME/.vim/plugin or $VIM/vimfiles/plugin directory.
" ****************** Do not modify after this line ************************
if exists('loaded_pdb')
    finish
endif
let loaded_pdb=1

if v:version < 700
    finish
endif

" Line continuation used here
let s:cpo_save = &cpo
set cpo&vim

" PDM configuration variables {{{1
if !exists('g:PDM_Dir')
    if has("win32")
        let g:PDM_Dir = $HOME . "/_vim_pdm"
    else
        let g:PDM_Dir = $HOME . "/.vim_pdm"
    endif
endif

if !exists('g:PDM_Rootpath')
    let g:PDM_Rootpath = getcwd()
endif

" WarnMsg                       {{{1
" Display a warning message
function! s:WarnMsg(msg)
    echohl WarningMsg
    echon a:msg
    echohl None
endfunction

" FileOf                            {{{1
" Get file name of file 
function! s:FileOf(file)
    return strpart(a:file, 0, stridx(a:file, ' '))
endfunction

" TimeOf                            {{{1
" Get file time of file
function! s:TimeOf(file)
    return strpart(a:file, stridx(a:file, ' ')+1)
endfunction

" CreateFilelist                    {{{1
" Create file list
function! s:CreateFilelist(root, filelist)
    call system('find '.a:root.' -type f -name "*.[ch]" -printf "%p %T@\n" | sort -f > '.a:filelist)
endfunction

" CreateFiletags                    {{{1
" Create file tags for lookupfile plugin
function! s:CreateFiletags(filelist, filetags)
    let ftags = []
    call add(ftags, "!_TAG_FILE_SORTED\t2\t/2=foldcase/")
    let flist = readfile(a:filelist)
    for file in flist
        let filename = strpart(s:FileOf(file), strridx(s:FileOf(file), "/")+1)
        call add(ftags, filename."\t".s:FileOf(file)."\t1")
    endfor
    call writefile(ftags, a:filetags)
endfunction

" CreateCsfiles                     {{{1
" Create cscope filelist
function! s:CreateCsfiles(filelist, csfiles)
    let csf = []
    let flist = readfile(a:filelist)
    for file in flist
        call add(csf, s:FileOf(file))
    endfor
    call writefile(csf, a:csfiles)
endfunction

" TagsAddFile                       {{{1
" add tags of files in fileList
function! s:TagsAddFile(tagsFile, fileList)
    let ctagsArg = '--c++-kinds=+p --fields=+iaS --extra=+q'
    call system('ctags -a '.ctagsArg.' -f "'.a:tagsFile.'" -L -', join(a:fileList, "\n"))
endfunction

" TagsDelFile                       {{{1
" delete tags of files in fileList
function! s:TagsDelFile(tagsFile, fileList)
    let tmpFile = fnamemodify(a:tagsFile, ':p:h').'/.'.fnamemodify(a:tagsFile, ":t").'.tmp'
    call system('grep -Fv -f - "'.a:tagsFile.'" > "'.tmpFile.'"', join(a:fileList,"\n"))
    call rename(tmpFile, a:tagsFile)
endfunction

" UpdateTags                        {{{1
" Update files between oldFileList and newFileList
function! s:UpdateTags(tagsfile, oldFileList, newFileList)
    let oldFiles = readfile(a:oldFileList)
    let newFiles = readfile(a:newFileList)

    let nf = 0
    let of = 0
    while nf < len(newFiles) && of < len(oldFiles)
        if s:FileOf(newFiles[nf]) > s:FileOf(oldFiles[of])
            call s:TagsDelFile(a:tagsfile, [s:FileOf(oldFiles[of])])
            let of = of + 1
        elseif s:FileOf(newFiles[nf]) == s:FileOf(oldFiles[of])
            if s:TimeOf(newFiles[nf]) != s:TimeOf(oldFiles[of])
                call s:TagsDelFile(a:tagsfile, [s:FileOf(oldFiles[of])])
                call s:TagsAddFile(a:tagsfile, [s:FileOf(newFiles[nf])])
            endif
            let nf = nf + 1
            let of = of + 1
        elseif s:FileOf(newFiles[nf]) < s:FileOf(oldFiles[of])
            call s:TagsAddFile(a:tagsfile, [s:FileOf(newFiles[nf])])
            let nf = nf + 1
        endif
    endwhile

    while nf < len(newFiles)
        call s:TagsAddFile(a:tagsfile, [s:FileOf(newFiles[nf])])
        let nf = nf + 1
    endwhile

    while of < len(oldFiles)
        call s:TagsDelFile(a:tagsfile, [s:FileOf(oldFiles[of])])
        let of = of + 1
    endwhile
endfunction

" UpdateCurrFile                    {{{1
" update current file to tagsFile and fileList
function! s:UpdateCurrFile(fileList, tagsFile)
    let filename = expand("%:p")

    " update tags
    call s:TagsDelFile(a:tagsFile, [filename])
    call s:TagsAddFile(a:tagsFile, [filename])

    " update filelist
    let files = readfile(a:fileList)
    let idx = 0
    while idx < len(files)
        if filename > s:FileOf(files[idx])
            let idx = idx + 1
            continue
        endif
        if filename == s:FileOf(files[idx])
            call remove(files, idx)
        endif
        let item = system('find '.filename.' -type f -printf "%p %T@"')
        call insert(files, item, idx)
        call writefile(files, a:fileList)
        break
    endwhile
endfunction

" CreatePDM                         {{{1
" Create project database
function! s:CreatePDM(name, tags_flg, cs_flg)
    " Set variables
    let pdmpath  = g:PDM_Dir.'/'.a:name
    let pdmconf  = pdmpath.'/configs'
    let filelist = pdmpath.'/filelist'
    let filetags = pdmpath.'/filetags'
    let tagsfile = pdmpath.'/tags'
    let csfiles  = pdmpath.'/cs.files'
    let csout    = pdmpath.'/cs.out'
    let pdmroot  = getcwd()
    let loadtags = a:tags_flg
    let loadcs   = a:cs_flg

    " Check whether project exist and create project
    if isdirectory(pdmpath)
        let choice = confirm("Project already exist, continue anyway?", "&Yes\n&No")
        if choice == 1
            call system('rm -rf '.pdmpath)
        else
            return 0
        endif
    endif
    call mkdir(pdmpath, "p")
    redraw

    echo "Create ".a:name." ... "

    " Create project files.
    if executable('find')
        let configs = []
        call add(configs, pdmroot)
        call add(configs, loadtags)
        call add(configs, loadcs)
        call writefile(configs, pdmconf)
        call s:CreateFilelist(pdmroot, filelist)
    else
        call s:WarnMsg("failed(command 'find' not exist)")
        return 0
    endif

    " Create file tags for lookupfile plugin
    call s:CreateFiletags(filelist, filetags)

    " Create ctags database.
    if loadtags == 1
        if executable('ctags')
            call system('ctags -R --c++-kinds=+p --fields=+iaS --extra=+q -o '.tagsfile.' '.pdmroot)
        else
            call s:WarnMsg("failed(command 'ctags' not exist)")
            return 0
        endif
    endif

    " Create cscope database.
    if loadcs == 1
        if executable('cscope')
            call s:CreateCsfiles(filelist, csfiles)
            call system('cscope -bqk -i'.csfiles.' -f'.csout)
        else
            call s:WarnMsg("failed(command 'cscope' not exist)")
            return 0
        endif
    endif

    echon "done"
    return 1
endfunction
" UpdatePDM                         {{{1
" Update project database
function! s:UpdatePDM(name)
    " Set variables
    let pdmpath     = g:PDM_Dir.'/'.a:name
    let pdmconf     = pdmpath.'/configs'
    let filelist    = pdmpath.'/filelist'
    let newFileList = pdmpath.'/filelist.new'
    let filetags    = pdmpath.'/filetags'
    let tagsfile    = pdmpath.'/tags'
    let csfiles     = pdmpath.'/cs.files'
    let csout       = pdmpath.'/cs.out'
    let pdmroot     = getcwd()
    let loadtags    = 0
    let loadcs      = 0

    echo "Update ".a:name." ... "

    if filereadable(pdmconf)
        let configs = readfile(pdmconf)
        let pdmroot = configs[0]
        let loadtags = strpart(configs[1], 0, 1)
        let loadcs = strpart(configs[2], 0, 1)
    else
        call s:WarnMsg("failed(file 'configs' not exist)")
        return 0
    endif

    " Update filelist
    if executable('find')
        call s:CreateFilelist(pdmroot, newFileList)
    else
        call s:WarnMsg("failed(command 'find' not exist)")
        return 0
    endif

    " Update filetags
    call s:CreateFiletags(newFileList, filetags)

    " Update ctags
    if loadtags == 1
        if executable('ctags')
            call s:UpdateTags(tagsfile, filelist, newFileList)
        else
            call s:WarnMsg("failed(command 'ctags' not exist)")
            return 0
        endif
    endif

    " Create cscope database.
    if loadcs == 1
        if executable('cscope')
            call s:CreateCsfiles(newFileList, csfiles)
            call system('cscope -bqk -i'.csfiles.' -f'.csout)
        else
            call s:WarnMsg("failed(command 'cscope' not exist)")
            return 0
        endif
    endif

    call rename(newFileList, filelist)
    echon "done"
    return 1
endfunction

" LoadPDM                           {{{1
" Load project database
function! s:LoadPDM(name)
    " Set variables
    let pdmpath  = g:PDM_Dir.'/'.a:name
    let pdmconf  = pdmpath.'/configs'
    let filelist = pdmpath.'/filelist'
    let filetags = pdmpath.'/filetags'
    let tagsfile = pdmpath.'/tags'
    let csfiles  = pdmpath.'/cs.files'
    let csout    = pdmpath.'/cs.out'
    let pdmroot  = getcwd()
    let loadtags = 0
    let loadcs   = 0

    echo "Load ".a:name." ... "

    if filereadable(pdmconf)
        let configs = readfile(pdmconf)
        let pdmroot = configs[0]
        let g:PDM_Rootpath = pdmroot
        let loadtags = strpart(configs[1], 0, 1)
        let loadcs = strpart(configs[2], 0, 1)
    else
        call s:WarnMsg("failed(file 'configs' not exist)")
        return 0
    endif

    " load filetags
    if filereadable(filetags)
        let g:LookupFile_TagExpr='"'.filetags.'"'
    else
        call s:WarnMsg("failed(file 'filetags' not exist)")
        return 0
    endif

    " load tags
    if loadtags == 1
        if filereadable(tagsfile)
            let &tags = tagsfile . "," . &tags
            let s:tagsFile = tagsfile
            let s:fileList = filelist
            aug PDM
                au BufWritePost * call s:UpdateCurrFile(s:fileList, s:tagsFile)
            aug END
        else
            call s:WarnMsg("failed(file 'tags' not exist)")
            return 0
        endif
    endif

    " load cscope
    if loadcs == 1
        if has("cscope") && filereadable(csout)
            set csto=1
            set cst
            set nocsverb
            exe "cs add ".csout
            cs reset
            set csverb
        else
            call s:WarnMsg("failed(file 'cscope.out' not exist)")
            return 0
        endif
    endif

    echon "done"
    return 1
endfunction

" PDM_Load                          {{{1
" Load project database
function! s:PDM_Load(name)
    call s:LoadPDM(a:name)
endfunction

" PDM_Update                        {{{1
" Update project database
function! s:PDM_Update(name)
    call s:UpdatePDM(a:name)
    call s:LoadPDM(a:name)
endfunction

" PDM_Create                        {{{1
" Create project database
function! s:PDM_Create(...)
    " Check parameters
    if a:0 != 3
        call s:WarnMsg("Usage: PDMCreate name tagsflag csflag")
        return
    endif

    if s:CreatePDM(a:1, a:2, a:3) == 0
        return
    endif
    call s:LoadPDM(a:1)
endfunction

" PDM autocommands {{{1
" Command of project database
command! -nargs=* -complete=file PDMCreate call s:PDM_Create(<f-args>)
command! -nargs=1 -complete=file PDMLoad call s:PDM_Load(<f-args>)
command! -nargs=1 -complete=file PDMUpdate call s:PDM_Update(<f-args>)

nmap <Leader>dc :PDMCreate 
nmap <Leader>dl :PDMLoad 
nmap <Leader>du :PDMUpdate 
nmap <Leader>dr :exe 'cd '.g:PDM_Rootpath<cr>:pwd<cr>
nmap <Leader>df :exe 'cd %:h'<cr>:pwd<cr>

" }}}

" restore 'cpo'
let &cpo = s:cpo_save
unlet s:cpo_save

" vim:set foldenable foldmethod=marker:
